/*
 * Copyright 2010-2023 JetBrains s.r.o. and Kotlin Programming Language contributors.
 * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
 */

package org.jetbrains.kotlin.generators.arguments

import org.jetbrains.kotlin.cli.common.arguments.*
import org.jetbrains.kotlin.config.CompilerSettings
import org.jetbrains.kotlin.config.JpsPluginSettings
import org.jetbrains.kotlin.config.LanguageFeature
import org.jetbrains.kotlin.utils.Printer
import java.io.File
import kotlin.reflect.KClass
import kotlin.reflect.KClassifier
import kotlin.reflect.KProperty1
import kotlin.reflect.KType
import kotlin.reflect.full.declaredMemberProperties
import kotlin.reflect.full.hasAnnotation
import kotlin.reflect.full.superclasses

private val CLASSES_TO_PROCESS: List<KClass<*>> = listOf(
    JpsPluginSettings::class,
    CompilerSettings::class,
    K2MetadataCompilerArguments::class,
    K2NativeCompilerArguments::class,
    K2JSCompilerArguments::class,
    K2WasmCompilerArguments::class,
    K2JVMCompilerArguments::class,
)

private val PACKAGE_TO_DIR_MAPPING: Map<Package, File> = mapOf(
    K2JVMCompilerArguments::class.java.`package` to File("compiler/cli/cli-common/gen"),
    JpsPluginSettings::class.java.`package` to File("jps/jps-common/gen"),
    CommonCompilerArguments::class.java.`package` to File("compiler/cli/cli-common/gen"),
)

fun generateCompilerArgumentsCopy(withPrinterToFile: (targetFile: File, Printer.() -> Unit) -> Unit) {
    val processed = mutableSetOf<KClass<*>>()
    for (klass in CLASSES_TO_PROCESS) {
        generateRec(klass, withPrinterToFile, processed)
    }
}

private val GENERATED_FILE_WARNING = """
    // DO NOT EDIT MANUALLY!
    // Generated by generators/tests/org/jetbrains/kotlin/generators/arguments/GenerateCompilerArgumentsCopy.kt
    // To regenerate run 'generateCompilerArgumentsCopy' task
""".trimIndent()

private fun generateRec(
    klass: KClass<*>,
    withPrinterToFile: (targetFile: File, Printer.() -> Unit) -> Unit,
    processed: MutableSet<KClass<*>>,
) {
    if (!processed.add(klass)) return

    val klassName = klass.simpleName!!
    val fqn = klass.qualifiedName!!
    val `package` = klass.java.`package`
    val destDir = PACKAGE_TO_DIR_MAPPING[`package`]!!.resolve(`package`.name.replace('.', '/'))
    withPrinterToFile(destDir.resolve(klassName + "CopyGenerated.kt")) {
        println("@file:Suppress(\"unused\", \"DuplicatedCode\")\n")
        println(GENERATED_FILE_WARNING + "\n")
        println("package ${`package`.name}\n")

        fun isSupportedImmutable(type: KType): Boolean {
            val classifier: KClassifier = type.classifier!!
            return when {
                classifier is KClass<*> && classifier == List::class -> isSupportedImmutable(type.arguments.single().type!!)
                classifier == ManualLanguageFeatureSetting::class -> true
                classifier == Boolean::class -> true
                classifier == Int::class -> true
                classifier == String::class -> true
                else -> false
            }
        }

        println("@OptIn(org.jetbrains.kotlin.utils.IDEAPluginsCompatibilityAPI::class)")
        println("fun copy$klassName(from: $klassName, to: $klassName): $klassName {")
        withIndent {
            val superClasses: List<KClass<*>> = klass.superclasses.filterNot { it.java.isInterface }
            check(superClasses.size < 2) {
                "too many super classes in $klass: ${superClasses.joinToString()}"
            }

            val superKlass = superClasses.singleOrNull()
            if (superKlass != null && superKlass != Freezable::class) {
                generateRec(superKlass, withPrinterToFile, processed)
                if (superKlass.java.`package` != `package`) {
                    print("${superKlass.java.`package`.name}.")
                }
                println("copy${superKlass.simpleName}(from, to)")
                println()
            }

            val properties = collectProperties(klass, false)

            for (property in properties.filter { klass.declaredMemberProperties.contains(it) }) {
                val type = property.returnType
                val classifier: KClassifier = type.classifier!!
                when {
                    // Please add cases on the go
                    // Please add a test to GenerateCompilerArgumentsCopyTest if the change is not trivial

                    classifier is KClass<*> && classifier.java.isArray -> {
                        val arrayElementType = type.arguments.single().type!!
                        val nullableMarker = if (type.isMarkedNullable) "?" else ""
                        when (arrayElementType.classifier) {
                            String::class -> {
                                deprecatePropertyIfNecessary(property)
                                println("to.${property.name} = from.${property.name}${nullableMarker}.copyOf()")
                            }
                            else -> error("Unsupported array element type $arrayElementType (member '${property.name}' of $fqn)")
                        }
                    }

                    isSupportedImmutable(type) -> {
                        deprecatePropertyIfNecessary(property)
                        println("to.${property.name} = from.${property.name}")
                    }

                    else -> error("Unsupported type to copy: $type (member '${property.name}' of $fqn)")
                }
            }

            println()
            println("return to")
        }
        println("}")
    }
}

private fun Printer.deprecatePropertyIfNecessary(property: KProperty1<*, *>) {
    if (property.hasAnnotation<Deprecated>()) {
        println("@Suppress(\"DEPRECATION\")")
    }
}

fun generateConfigureLanguageFeatures(withPrinterToFile: (targetFile: File, Printer.() -> Unit) -> Unit) {
    generateConfigureLanguageFeaturesImpl(CommonCompilerArguments::class.java, "Common", withPrinterToFile)
    generateConfigureLanguageFeaturesImpl(K2JVMCompilerArguments::class.java, "Jvm", withPrinterToFile)
    generateConfigureLanguageFeaturesImpl(K2JSCompilerArguments::class.java, "Js", withPrinterToFile)
}

private fun generateConfigureLanguageFeaturesImpl(
    compilerArgumentsClass: Class<*>,
    qualifier: String,
    withPrinterToFile: (targetFile: File, Printer.() -> Unit) -> Unit,
) {
    val targetPackage = compilerArgumentsClass.`package`
    val destDir = PACKAGE_TO_DIR_MAPPING[targetPackage]!!.resolve(targetPackage.name.replace('.', '/'))

    withPrinterToFile(destDir.resolve("Configure${qualifier}LanguageFeatures.kt")) {
        println(GENERATED_FILE_WARNING + "\n")
        println("package ${targetPackage.name}\n")
        println("import org.jetbrains.kotlin.config.LanguageFeature\n")
        println("internal fun MutableMap<LanguageFeature, LanguageFeature.State>.configure${qualifier}LanguageFeatures(arguments: ${compilerArgumentsClass.simpleName}) {")

        withIndent {
            enableFeaturesFromDeclaredFieldsOf(compilerArgumentsClass)
        }

        println("}")
    }
}

private fun Printer.enableFeaturesFromDeclaredFieldsOf(klass: Class<*>) {
    var isFirst = true

    for (field in klass.declaredFields) {
        if (field.getAnnotation(Argument::class.java) == null) continue

        val featuresByValue = buildMap<String, Pair<MutableList<LanguageFeature>, MutableList<LanguageFeature>>> {
            for (enables in field.getAnnotationsByType(Enables::class.java)) {
                val pair = getOrPut(enables.ifValueIs) { Pair(mutableListOf(), mutableListOf()) }
                pair.first.add(enables.feature)
            }
            for (disables in field.getAnnotationsByType(Disables::class.java)) {
                val pair = getOrPut(disables.ifValueIs) { Pair(mutableListOf(), mutableListOf()) }
                pair.second.add(disables.feature)
            }
        }

        if (featuresByValue.isEmpty()) continue

        if (!isFirst) {
            println()
        }

        for ((value, pair) in featuresByValue) {
            val (featuresToEnable, featuresToDisable) = pair

            val optionalComparison = if (value.isNotBlank()) " == \"$value\"" else ""
            println("if (arguments.${field.name}$optionalComparison) {")
            withIndent {
                for (feature in featuresToEnable) {
                    println("put(LanguageFeature.${feature.name}, LanguageFeature.State.ENABLED)")
                }

                for (feature in featuresToDisable) {
                    println("put(LanguageFeature.${feature.name}, LanguageFeature.State.DISABLED)")
                }
            }
            println("}")
        }

        isFirst = false
    }
}

fun main() {
    generateCompilerArgumentsCopy(::getPrinterToFile)
    generateConfigureLanguageFeatures(::getPrinterToFile)
}
